package com.xiaoxu.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.j256.simplemagic.ContentType;
import com.xiaoxu.base.exception.bkSchoolException;
import com.xiaoxu.base.model.PageParams;
import com.xiaoxu.base.model.PageResult;
import com.xiaoxu.base.model.RestResponse;
import com.xiaoxu.base.utils.MinIOFilePathUtil;
import com.xiaoxu.media.dao.MediaFilesMapper;
import com.xiaoxu.media.dao.MediaProcessMapper;
import com.xiaoxu.media.model.dto.QueryMediaParamsDto;
import com.xiaoxu.media.model.dto.UploadFileParamsDto;
import com.xiaoxu.media.model.dto.UploadResultMediaDto;
import com.xiaoxu.media.model.po.MediaFiles;
import com.xiaoxu.media.model.po.MediaProcess;
import com.xiaoxu.media.service.MediaFileService;
import io.minio.*;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;

/**
 * @author xiaoxu
 * @version 1.0
 * @description 媒资文件系统业务层
 * @date 2023/1/20 15:58
 */
@Service
@Slf4j
public class MediaFileServiceImpl extends ServiceImpl<MediaFilesMapper, MediaFiles> implements MediaFileService {

    private final MediaFilesMapper mediaFilesMapper;
    private final MinioClient minioClient;

    private final MediaProcessMapper mediaProcessMapper;
    /**
     * 注意：一定不要使用构造器注入，否则Spring的三级缓存无法解决循环依赖
     */
    private final MediaFileService currentProxy;

    /**
     * 普通文件桶
     */
    @Value("${minio.bucket.files}")
    private String bucket_Files;

    @Value("${minio.bucket.videofiles}")
    private String bucket_videoFiles;


    public MediaFileServiceImpl(MediaFilesMapper mediaFilesMapper, MinioClient minioClient, MediaProcessMapper mediaProcessMapper, @Lazy MediaFileService currentProxy) {
        this.mediaFilesMapper = mediaFilesMapper;
        this.minioClient = minioClient;
        this.mediaProcessMapper = mediaProcessMapper;
        this.currentProxy = currentProxy;
    }

    /**
     * 查询文件存储数据库分页信息
     *
     * @param companyId           机构Id
     * @param pageParams          分页参数
     * @param queryMediaParamsDto 查询条件
     * @return PageResult<MediaFiles>
     */
    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
        //构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(StringUtils.isNotEmpty(queryMediaParamsDto.getFileType()), MediaFiles::getFileType, queryMediaParamsDto.getFileType());
        queryWrapper.like(StringUtils.isNotEmpty(queryMediaParamsDto.getFilename()), MediaFiles::getFilename, queryMediaParamsDto.getFilename());
        queryWrapper.eq(StringUtils.isNotEmpty(queryMediaParamsDto.getAuditStatus()), MediaFiles::getAuditStatus, queryMediaParamsDto.getAuditStatus());
        //分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        return new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
    }

    /**
     * 上传文件的服务方法
     *
     * @param companyId  机构Id
     * @param fileBytes  字节数组
     * @param params     文件参数
     * @param folder     文件目录
     * @param objectName 文件名称
     * @return UploadResultMediaDto
     */
    @Override
    public UploadResultMediaDto uploadFile(Long companyId, byte[] fileBytes, UploadFileParamsDto params, String folder, String objectName) {
        //生成文件id，文件的md5值
        String fileId = DigestUtils.md5Hex(fileBytes);
        //文件名称
        String filename = params.getFilename();
        //构造objectName
        if (StringUtils.isEmpty(objectName)) {
            objectName = fileId + filename.substring(filename.lastIndexOf("."));
        }
        if (StringUtils.isEmpty(folder)) {
            //通过日期构造文件存储路径
            folder = MinIOFilePathUtil.getFileFolder(new Date(), true, true, true);
        } else if (!folder.contains("/")) {
            folder = folder + "/";
        }
        //对象名称
        objectName = folder + objectName;
        try {
            uploadFileToMinio(fileBytes, bucket_Files, objectName);
            //通过代理对象开启事物管理   currentProxy ： spring代理的自己
            MediaFiles mediaFiles = currentProxy.addFileToDB(companyId, fileId, params, bucket_Files, objectName);
            UploadResultMediaDto uploadFileResultDto = new UploadResultMediaDto();
            BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
            return uploadFileResultDto;
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("上传文件操作错误: " + e.getMessage());
            throw new bkSchoolException(e.getMessage());
        }
    }

    /**
     * 将文件数据信息封装到数据库
     *
     * @param companyId  上传文件的机构Id
     * @param fileId     文件统一标识: 文件MD5码
     * @param params     上传文件的请求参数
     * @param bucketName 文件系统的桶名称
     * @param objectName 对象名称: 路径 + 文件名 + 后缀
     * @return MediaFiles
     */
    @Transactional
    public MediaFiles addFileToDB(Long companyId, String fileId, UploadFileParamsDto params, String bucketName, String objectName) {
        MediaFiles mediaFiles;
        //从数据库查询文件
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(MediaFiles::getFileId, fileId);
        mediaFiles = mediaFilesMapper.selectOne(queryWrapper);
        if (mediaFiles == null) {
            mediaFiles = new MediaFiles();
            //拷贝基本信息
            BeanUtils.copyProperties(params, mediaFiles);
            mediaFiles.setFileId(fileId);
            mediaFiles.setCompanyId(companyId);
            mediaFiles.setUrl("/" + bucketName + "/" + objectName);
            mediaFiles.setFilePath(objectName);
            mediaFiles.setBucket(bucketName);
            mediaFiles.setCreateDate(LocalDateTime.now());
            mediaFiles.setStatus("1");
            //保存文件信息到文件表
            int insert = mediaFilesMapper.insert(mediaFiles);
            if (insert < 0) {
                throw new bkSchoolException("保存文件信息失败");
            }
            //把avi视频添加到待处理任务表
            String fileType = mediaFiles.getFileType();
            String contentType = params.getContentType();
            if("001002".equals(fileType) && ContentType.AVI.getMimeType().equals(contentType)){
                MediaProcess mediaProcess = new MediaProcess();
                BeanUtils.copyProperties(mediaFiles,mediaProcess);
                //设置一个状态
                mediaProcess.setStatus("1");  // 1代表未处理
                mediaProcessMapper.insert(mediaProcess);
            }
        }
        return mediaFiles;
    }

    /**
     * 检查文件是否存在业务操作
     *
     * @param fileMd5 文件的md5
     * @return RestResponse<Boolean>
     */
    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        //查询是否在数据库中存在
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(MediaFiles::getFileId, fileMd5);
        MediaFiles mediaFiles = mediaFilesMapper.selectOne(queryWrapper);
        if (mediaFiles == null) {
            return RestResponse.success(false);
        }
        try {
            //查询是否在文件系统中存在
            InputStream inputStream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(mediaFiles.getBucket())
                            .object(mediaFiles.getFilePath())
                            .build()
            );
            if (inputStream == null) {
                //找不到文件，删除数据库信息，返回不存在
                currentProxy.deleteDBFile(fileMd5);
                return RestResponse.success(false);
            }
        } catch (Exception e) {
            //找不到文件，删除数据库信息，返回不存在
            currentProxy.deleteDBFile(fileMd5);
            return RestResponse.success(false);
        }
        //文件存在
        return RestResponse.success(true);
    }

    /**
     * 检查分块是否存在业务操作
     *
     * @param fileMd5    文件的md5
     * @param chunkIndex 分块序号
     * @return RestResponse<Boolean>
     */
    @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
        //得到分块文件目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunkIndex;
        //文件流
        InputStream fileInputStream;
        try {
            fileInputStream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucket_videoFiles)
                            .object(chunkFilePath)
                            .build());

            if (fileInputStream != null) {
                //分块已存在
                return RestResponse.success(true);
            }
        } catch (Exception e) {
            return RestResponse.success(false);
        }
        //分块未存在
        return RestResponse.success(false);
    }

    /**
     * 上传分块业务操作
     *
     * @param fileMd5 文件md5
     * @param chunk   分块序号
     * @param bytes   文件字节
     * @return RestResponse
     */
    @Override
    public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {
        //得到分块文件的目录路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunk;
        try {
            //将文件存储至minIO
            uploadFileToMinio(bytes, bucket_videoFiles, chunkFilePath);
        } catch (Exception ex) {
            ex.printStackTrace();
            log.debug("业务出错了，上传过程出错请重试，详细 : " + ex.getMessage());
            throw new bkSchoolException("上传过程出错请重试");
        }
        return RestResponse.success();
    }

    /**
     * 合并分块业务操作
     *
     * @param companyId           机构id
     * @param fileMd5             文件md5
     * @param chunkTotal          分块总和
     * @param uploadFileParamsDto 文件信息
     * @return RestResponse
     */
    @Override
    public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
        //获取文件名
        String fileName = uploadFileParamsDto.getFilename();
        //下载所有分块文件
        File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);
        //扩展名
        String extName = fileName.substring(fileName.lastIndexOf("."));
        //创建临时文件作为合并文件
        File mergeFile = null;
        try {
            mergeFile = File.createTempFile(fileMd5, extName);
        } catch (IOException e) {
            bkSchoolException.cast("合并文件过程中创建临时文件出错");
        }
        try {
            //开始合并
            byte[] b = new byte[1024];
            try (RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw")) {
                for (File chunkFile : chunkFiles) {
                    try (FileInputStream chunkFileStream = new FileInputStream(chunkFile)) {
                        int len;
                        while ((len = chunkFileStream.read(b)) != -1) {
                            //向合并后的文件写
                            raf_write.write(b, 0, len);
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                bkSchoolException.cast("合并文件过程中出错");
            }
            log.debug("合并文件完成,文件保存在了: "+ mergeFile.getAbsolutePath());
            uploadFileParamsDto.setFileSize(mergeFile.length());
            try (InputStream mergeFileInputStream = Files.newInputStream(mergeFile.toPath())) {
                //对文件进行校验，通过比较md5值
                String newFileMd5 = DigestUtils.md5Hex(mergeFileInputStream);
                if (!fileMd5.equalsIgnoreCase(newFileMd5)) {
                    //校验失败
                    bkSchoolException.cast("合并文件校验失败");
                }
                log.debug("合并文件校验通过,文件保存在了: "+mergeFile.getAbsolutePath());
            } catch (Exception e) {
                e.printStackTrace();
                //校验失败
                bkSchoolException.cast("合并文件校验异常");
            }
            //将临时文件上传至minio
            String mergeFilePath = MinIOFilePathUtil.getFilePathByMd5(fileMd5, extName);
            try {
                //上传文件到minIO
                uploadMediaFilesToMinIO(mergeFile.getAbsolutePath(), bucket_videoFiles, mergeFilePath);
                log.debug("合并文件上传MinIO完成,文件保存在了:"+mergeFile.getAbsolutePath());
            } catch (Exception e) {
                e.printStackTrace();
                bkSchoolException.cast("合并文件时上传文件出错");
            }

            //入数据库
            MediaFiles mediaFiles = currentProxy.addFileToDB(companyId, fileMd5, uploadFileParamsDto, bucket_videoFiles, mergeFilePath);
            if (mediaFiles == null) {
                bkSchoolException.cast("媒资文件入库出错");
            }
            return RestResponse.success();
        } finally {
            //删除临时文件
            for (File file : chunkFiles) {
                try {
                    boolean delete = file.delete();
                    if (!delete){
                        log.error("文件删除异常，请检查异常，否则会导致系统出错");
                    }
                } catch (Exception e) {
                    log.error("文件删除异常，请检查异常，否则会导致系统出错");
                }
            }
            try {
                boolean delete = mergeFile.delete();
                if (!delete){
                    log.error("文件删除异常，请检查异常，否则会导致系统出错");
                }
            } catch (Exception e) {
                log.error("文件删除异常，请检查异常，否则会导致系统出错");
            }
        }
    }

    /**
     * 上传文件到分布式文件系统
     *
     * @param bytes      文件字节数组
     * @param bucketName 分布式系统的桶名称
     * @param objectName 对象名称 : 路径 + 文件名 + 后缀
     */
    public void uploadFileToMinio(byte[] bytes, String bucketName, String objectName) {
        try {
            //资源的媒体类型  默认是未知的媒体流类型
            String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
            if (objectName.contains(".")) {
                //获取objectName的扩展名
                String extension = objectName.substring(objectName.lastIndexOf("."));
                ContentInfo extensionContentInfo = ContentInfoUtil.findExtensionMatch(extension);
                if (extensionContentInfo != null) {
                    contentType = extensionContentInfo.getMimeType();
                }
            }
            //转为流
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
            //构造上传参数
            PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucketName).object(objectName)
                    //-1表示文件分片按5M(不小于5M,不大于5T),分片数量最大10000，
                    .stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
                    .contentType(contentType)
                    .build();
            //上传文件
            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("上传文件到文件系统出错了: " + e.getMessage());
            throw new bkSchoolException(e.getMessage());
        }
    }



    /**
     * 根据文件MD5码删除数据库文件信息
     *
     * @param md5 文件MD5码
     */
    @Transactional
    public void deleteDBFile(String md5) {
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(MediaFiles::getFileId, md5);
        MediaFiles mediaFiles = mediaFilesMapper.selectOne(queryWrapper);
        if (mediaFiles == null) {
            throw new bkSchoolException("业务异常，文件信息找不到");
        }
        int count = mediaFilesMapper.delete(queryWrapper);
        if (count <= 0) {
            throw new bkSchoolException("业务异常，删除数据库文件信息失败");
        }
        //删除成功则将minio文件系统中的文件也同时删除
        String bucket = mediaFiles.getBucket();
        String filePath = mediaFiles.getFilePath();
        if (bucket.equals("video")) {
            //视频文件就将整个目录删除
            filePath = filePath.substring(0, filePath.lastIndexOf("/")+1);
        }
        removeFileFromMinIO(bucket,filePath);
    }

    /**
     * 根据Id查找文件信息
     * @param id 文件Id
     * @return 文件信息
     */
    @Override
    public MediaFiles getFileById(Long id) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(id);
        if (mediaFiles == null){
            throw new bkSchoolException("业务异常，文件信息找不到");
        }
        return mediaFiles;
    }

    @Override
    public void deleteDBFile(Long id) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(id);
        if (mediaFiles == null){
            throw new bkSchoolException("业务异常，文件信息找不到");
        }
        int count = mediaFilesMapper.deleteById(id);
        if (count <=0){
            throw new bkSchoolException("删除文件信息失败");
        }
        //删除成功则将minio文件系统中的文件也同时删除
        String bucket = mediaFiles.getBucket();
        String filePath = mediaFiles.getFilePath();
        if (bucket.equals("video")) {
            //视频文件就将整个目录删除
            filePath = filePath.substring(0, filePath.lastIndexOf("/")+1);
        }
        removeFileFromMinIO(bucket,filePath);
    }

    /**
     * 得到分块文件的目录
     *
     * @param fileMd5 文件MD5值
     * @return String
     */
    private String getChunkFileFolderPath(String fileMd5) {
        String folder = MinIOFilePathUtil.getFileFolder(new Date(), true, true, true);
        return folder + fileMd5 + "/" + "chunk" + "/";
    }

    /**
     * 检查所有分块是否上传完毕，并下载
     * @param fileMd5    文件MD5码
     * @param chunkTotal 分块总量
     * @return File[]
     */
    private File[] checkChunkStatus(String fileMd5, int chunkTotal) {
        //得到分块文件的目录路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        File[] files = new File[chunkTotal];
        //检查分块文件是否上传完毕
        for (int i = 0; i < chunkTotal; i++) {
            String chunkFilePath = chunkFileFolderPath + i;
            //下载文件
            File chunkFile;
            try {
                chunkFile = File.createTempFile("chunk" + i, null);
            } catch (IOException e) {
                e.printStackTrace();
                throw new bkSchoolException("下载分块时创建临时文件出错");
            }
            downloadFileFromMinIO(chunkFile, bucket_videoFiles, chunkFilePath);
            files[i] = chunkFile;
        }
        return files;
    }

    /**
     * 从分布式文件系统下载文件
     * @param file 文件路径
     * @param bucket 桶
     * @param objectName  文件名
     */
    public void downloadFileFromMinIO(File file, String bucket, String objectName) {
        InputStream fileInputStream = null;
        OutputStream fileOutputStream = null;
        try {
            fileInputStream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucket)
                            .object(objectName)
                            .build());
            try {
                fileOutputStream = Files.newOutputStream(file.toPath());
                IOUtils.copy(fileInputStream, fileOutputStream);
            } catch (IOException e) {
                throw new bkSchoolException("下载文件" + objectName + "出错");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new bkSchoolException("文件不存在" + objectName);
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }



    /**
     * 将文件上传到minIO，传入文件绝对路径
     * @param filePath 文件路径
     * @param bucket 桶名称
     * @param objectName 对象名
     */
    private void uploadMediaFilesToMinIO(String filePath, String bucket, String objectName) {
        try {
            minioClient.uploadObject(
                    UploadObjectArgs.builder()
                            .bucket(bucket)
                            .object(objectName)
                            .filename(filePath)
                            .build());
        } catch (Exception e) {
            e.printStackTrace();
            throw new bkSchoolException("上传文件到文件系统出错");
        }
    }

    /**
     * @description 从MinIo里删除文件
     * @param bucket 桶名称
     * @param path 路径
     */
    private void removeFileFromMinIO(String bucket,String path){
        try {
            //删除文件或文件夹
            if (StringUtils.isNotBlank(bucket) && StringUtils.isNotBlank(path)) {
                if (path.endsWith("/")) {
                    //删除文件夹
                    Iterable<Result<Item>> list = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucket).prefix(path).build());
                    for (Result<Item> item : list) {
                        if (item.get().isDir()){
                            removeFileFromMinIO(bucket,item.get().objectName());
                        }else {
                            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(item.get().objectName()).build());
                        }

                    }
                }else{
                    //删除文件
                    minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(path).build());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("文件系统出错了，删除文件失败 : {}",e.getMessage());
            throw new bkSchoolException(e.getMessage());
        }
    }

}
